package com.ss.zsensm;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import android.graphics.Rect;
import android.util.Log;

public class DataTransmission implements Runnable {
	private DatagramSocket sock;
	private DataCollector dc;
	private MainActivity ui;
	private Thread dcThread;
 
	public DataTransmission(MainActivity ui, DataCollector dc) {
		this.dc = dc;
		this.ui = ui;
		sock = null;
		dcThread = null;
	}
	
	// called by data transmission thread
	public void startDataCollect() {
		dcThread = new Thread(dc);
		dcThread.start();
	}
	
	// called by data transmission thread
	public void stopDataCollect() {
		dc.stop();
		try {
			dcThread.join();
		} catch (InterruptedException e) {
			Log.e("zsensm", "dt.sdc.ie:" + e.getMessage());
			return;
		}
	}
	
	// called by mainActivity
	public void interruptSocket() {
		try {
			sock.close(); // if needed
		}
		catch (Exception e) {
			Log.e("zsensm", "dt.is: " + e.toString());
		}
	}
	
	public boolean validate(byte[] buf, int start, int length, int hash) {
		return (Serializing.hash(buf, start, start + length) == hash);
	}
	
	public boolean validatePacket(byte[] buf, int length) {
		Serializing.shallowCopyFromByteArray(buf, length);
		int hash = Serializing.getInt(1);
		if (Serializing.error())
			return false;
		return validate(buf, 5, length - 5, hash);
	}
	
	public static int MakeDetectPacket(byte[] buf) {
		buf[0] = PacketInfo.ZS_DETECT;
		return 1;
	}
	
	public static int MakeMatchPacket(byte[] buf, String deviceName) {
		buf[0] = PacketInfo.ZS_MATCH;
		int end = Serializing.put(buf, 5, deviceName);
		if (end < 0)
			return -1;
		Serializing.put(buf, 1, Serializing.hash(buf, 5, end));
		return end;
	}
	
	public Rect parseRect() {
		int ltx, lty, rbx, rby;
		ltx = Serializing.getInt();
		if (Serializing.error()) 
			return null;
		lty = Serializing.getInt();
		if (Serializing.error()) 
			return null;
		rbx = Serializing.getInt();
		if (Serializing.error()) 
			return null;
		rby = Serializing.getInt();
		if (Serializing.error()) 
			return null;
		return new Rect(ltx, lty, rbx, rby);
	}
	
	// guarantee buf loaded into serializing
	public boolean parseLayoutPacket() {
		String layoutName = Serializing.getString();
		int count = (int)Serializing.getByte();
		Log.i("zsensm", "widget count: " + count);
		if (Serializing.error())
			return false;
		for (int i = 0; i < count; i++) {
			int t = (int) Serializing.getByte();
			if (Serializing.error() || t != i)
				return false;
			int type = (int) Serializing.getByte();
			if (Serializing.error() || type > PacketInfo.ZS_WIDGET_HAT)
				return false;
			switch (type) {
			case PacketInfo.ZS_WIDGET_BUTTON:
				Rect rect;
				String name = Serializing.getString();
				if (Serializing.error())
					return false;
				if ((rect = parseRect()) == null)
					return false;
				ui.addWidget(type, new ZSButtonWidget(ui, name, rect));
				break;
			case PacketInfo.ZS_WIDGET_ORIENTATION_X:
				ui.addWidget(type, new ZSOrientationXWidget(ui));
				break;
			case PacketInfo.ZS_WIDGET_ORIENTATION_Y:
				ui.addWidget(type, new ZSOrientationYWidget(ui));
				break;
			default:
				return false;
			}
		}
		ui.setLayoutName(layoutName);
		return true;
	}
	
	// called by data transmission thread
	public boolean send(byte[] buf, int length, SocketAddress remote) {
		try {
			sock.send(new DatagramPacket(buf, length, remote));
		} catch (SocketException e) {
			Log.e("zsensm", "dt.send.se: " + e.toString());
			return false;
		} catch (IOException e) {
			Log.e("zsensm", "dt.send.se: " + e.toString());
			return false;
		}
		return true;
	}
	
	// called by data transmission thread
	@Override
	public void run() {	
		int length;
		byte tid;
		try {
			try {
				sock = new DatagramSocket();
			} catch (SocketException e) {
				Log.e("zsensm", "dt.run.socket_constructor: " + e.toString());
				return;
			}
			byte[] bufIn = new byte[1024];
			byte[] bufOut = new byte[1024];
			DatagramPacket dp = new DatagramPacket(bufIn, bufIn.length);

			// broadcast in LAN first
			try {
				sock.setBroadcast(true);
				length = MakeDetectPacket(bufOut);
				sock.send(new DatagramPacket(bufOut, length, InetAddress
						.getByName("255.255.255.255"), PacketInfo.port));
				sock.setBroadcast(false);
			} catch (SocketException e) {
				Log.e("zsensm", "nw.run.se: " + e.toString());
				return;
			} catch (UnknownHostException e) {
				Log.e("zsensm", "dt.run.nhe: " + e.toString());
				return;
			} catch (IOException e) {
				Log.e("zsensm", "dt.run.ioe: " + e.toString());
				return;
			}
			
			while (true) {
				try {
					sock.receive(dp);
				} catch (IOException e) {
					Log.e("zsensm", "dt.run.ioe: " + e.toString());
					break;
				}
				if (Thread.currentThread().isInterrupted()) {
					break;
				}
				byte[] dataIn = dp.getData();
				
				switch (dataIn[0]) {
				case PacketInfo.ZS_EXIST:
					if (dc.isStarted()) {
						Log.i("zsensm", "dt.run: ZS_EXIST(ignored)");
						break;
					}
					Log.i("zsensm", "dt.run: ZS_EXIST");
					length = MakeMatchPacket(bufOut, android.os.Build.MODEL);
					if (this.send(bufOut, length, new InetSocketAddress(dp.getAddress(), PacketInfo.port)) == false) {
						if (dc.getStatus() == DataCollector.Running) {
							stopDataCollect();
						}
						return;
					}
					break;
				case PacketInfo.ZS_ACK:
					if (dc.isStarted()) {
						Log.i("zsensm", "dt.run: ZS_ACK(ignored)");
						break;
					}
					Log.i("zsensm", "dt.run: ZS_ACK");
					if (!validatePacket(dataIn, dp.getLength())) 
						break;
					Log.i("zsensm", "dt.run: ZS_ACK.validated");
					tid = Serializing.getByte(5);
					if (Serializing.error())
						break;
					dc.setRemote(dp.getAddress(), (int) tid);
					break;
				case PacketInfo.ZS_LAYOUT:
					if (!dc.isStarted()) {
						Log.i("zsensm", "dt.run: ZS_LAYOUT(ignored)");
						break;
					}
					Log.i("zsensm", "dt.run: ZS_LAYOUT");
					if (!validatePacket(dataIn, dp.getLength()))
						break;
					Log.i("zsensm", "dt.run: ZS_LAYOUT.validated");
					tid = Serializing.getByte(5);
					if (dc.getPlayerId() != (int) tid || !dp.getAddress().equals(dc.getRemoteAddress()))
						break;
					if (dc.getStatus() == DataCollector.Pending) {
						Log.i("zsensm", "applying new layout");
						if (parseLayoutPacket()) {
							Log.i("zsensm", "applying new layout: success");
							ui.commitLayout((byte) dc.getPlayerId());
							startDataCollect();
						} else {
							Log.i("zsensm", "applying new layout: failed");
							ui.initialize();
						}
					} else if (dc.getStatus() == DataCollector.Running) {
						Log.i("zsensm", "shifting to new layout");
						stopDataCollect();
						ui.initializeLayout(); 
						synchronized (this) {
							this.wait(); // wait for ui thread to clear
						}
						if (parseLayoutPacket()) {
							ui.commitLayout((byte) dc.getPlayerId());
							startDataCollect();
						}
					}
					break;
				case PacketInfo.ZS_KICK:
					if (!dc.isStarted()) {
						Log.i("zsensm", "dt.run: ZS_KICK(ignored)");
						break;
					}
					Log.i("zsensm", "dt.run: ZS_KICK");
					if (!validatePacket(dataIn, dp.getLength()))
						break;
					Log.i("zsensm", "dt.run: ZS_KICK.validated");
					if (dc.getPlayerId() != (int) Serializing.getByte(5) || !dp.getAddress().equals(dc.getRemoteAddress()))
						break;
					if (dc.getStatus() == DataCollector.Pending) {
						dc.stop();
					} else if (dc.getStatus() == DataCollector.Running) {
						stopDataCollect();
					}
					break;
				default:
					break;
				}
			}
			
			if (dc.getStatus() == DataCollector.Running) {
				stopDataCollect();
			} else if (dc.getStatus() == DataCollector.Pending) {
				dc.stop();
			}
		} catch (Exception e) {
			Log.e("zsensm", "dt.run.uke: " + e.toString() + e.getMessage());
			return;
		}
	}
}
